/*
  author Sylvain Bertrand <sylvain.bertrand@gmail.com>
  Protected by linux GNU GPLv2
  Copyright 2012-2014
*/
#include <linux/pci.h>
#include <linux/cdev.h>
#include <asm/unaligned.h>

#include <alga/rng_mng.h>
#include <uapi/alga/pixel_fmts.h>
#include <alga/timing.h>
#include <alga/amd/atombios/atb.h>
#include <uapi/alga/amd/dce6/dce6.h>
#include <alga/amd/atombios/vm.h>
#include <alga/amd/atombios/cm.h>
#include <alga/amd/atombios/pp.h>
#include <alga/amd/atombios/vram_info.h>

#include "../mc.h"
#include "../rlc.h"
#include "../ih.h"
#include "../fence.h"
#include "../ring.h"
#include "../dmas.h"
#include "../ba.h"
#include "../cps.h"
#include "../gpu.h"
#include "../drv.h"

#include "../regs.h"

#include "../smc_tbls.h"

#include "ctx.h"
#include "private.h"
#include "initial.h"
#include "emergency.h"
#include "ulv.h"

#ifdef CONFIG_ALGA_AMD_SI_DYN_PM_LOG
#include "smc_lvl.h"
#define L(fmt,...) printk(KERN_INFO fmt "\n", ##__VA_ARGS__)
static void smc_sw_state_dump(struct smc_sw_state *state)
{
	L("flgs=0x%02x",state->flgs);
	L("lvls_n=0x%02x",state->lvls_n);
	L("pad2=0x%02x",state->pad2);
	L("pad3=0x%02x",state->pad3);
}

void smc_state_tbl_dump(struct smc_state_tbl *tbl)
{
	u32 i;
	u32 tmp;

	L("SMC_STATE_TBL START");
	L("thermal_protection_type=0x%02x",tbl->thermal_protection_type);
	L("system_flgs=0x%02x",tbl->system_flgs);
	L("max_vddc_idx=0x%02x",tbl->max_vddc_idx);
	L("extra_flgs=0x%02x",tbl->extra_flgs);

	for (i = 0; i < SMC_VREG_STEPS_N_MAX; ++i) {
		u32 smio_low;
		smio_low = get_unaligned_be32(&tbl->smio_low[i]);
		L("smio_low[%u]=0x%08x",i,smio_low);
	}

	for (i = 0; i < SMC_VOLT_MASKS_N_MAX; ++i) {
		u32 mask_low;
		mask_low = get_unaligned_be32(&tbl->volt_mask_tbl.mask_low[i]);
		L("volt_mask_tbl.mask_low[%u]=0x%08x",i,mask_low);
	}

	for (i = 0; i < SMC_VOLT_MASKS_N_MAX; ++i) {
		u32 mask_low;
		mask_low = get_unaligned_be32(&tbl->phase_mask_tbl.mask_low[i]);
		L("phase_mask_tbl.mask_low[%u]=0x%08x",i,mask_low);
	}

	tmp = get_unaligned_be32(&tbl->dyn_pm_params.tdp_limit);
	L("dyn_pm_params.tdp_limit=0x%08x",tmp);
	tmp = get_unaligned_be32(&tbl->dyn_pm_params.near_tdp_limit);
	L("dyn_pm_params.near_tdp_limit=0x%08x",tmp);
	tmp = get_unaligned_be32(&tbl->dyn_pm_params.safe_pwr_limit);
	L("dyn_pm_params.safe_pwr_limit=0x%08x",tmp);
	tmp = get_unaligned_be32(&tbl->dyn_pm_params.pwrboost_limit);
	L("dyn_pm_params.pwrboost_limit=0x%08x",tmp);
	tmp = get_unaligned_be32(&tbl->dyn_pm_params.min_limit_delta);
	L("dyn_pm_params.min_limit_delta=0x%08x",tmp);

	L("SMC_SW_STATE INITIAL START");
	smc_sw_state_dump(&tbl->initial);
	L("SMC_SW_STATE INITIAL END");

	L("SMC_LVL[0] INITIAL START");
	smc_lvl_dump(&tbl->initial_lvl);
	L("SMC_LVL[0] INITIAL END");

	L("SMC_SW_STATE EMERGENCY START");
	smc_sw_state_dump(&tbl->emergency);
	L("SMC_SW_STATE EMERGENCY END");

	L("SMC_LVL[0] EMERGENCY START");
	smc_lvl_dump(&tbl->emergency_lvl);
	L("SMC_LVL[0] EMERGENCY END");

	L("SMC_SW_STATE ULV START");
	smc_sw_state_dump(&tbl->ulv);
	L("SMC_SW_STATE ULV END");

	L("SMC_LVL[0] ULV START");
	smc_lvl_dump(&tbl->ulv_lvl);
	L("SMC_LVL[0] ULV END");

	L("SMC_SW_STATE DRIVER START");
	smc_sw_state_dump(&tbl->driver);
	L("SMC_SW_STATE DRIVER END");

	for (i = 0; i < tbl->driver.lvls_n; ++i) {
		L("SMC_LVL[%u] DRIVER START", i);
		smc_lvl_dump(&tbl->driver_lvls[i]);
		L("SMC_LVL[%u] DRIVER END", i);
	}
	
	L("SMC_STATE_TBL END");
}
#endif

static void smc_vddc_tbl_init(struct ctx *ctx,
					struct smc_state_tbl *smc_state_tbl)
{
	struct dev_drv_data *dd;
	u8 i;
	u16 vddc_mv_max;
	u8 max_vddc_idx;

	dd = pci_get_drvdata(ctx->dev);

	LOG("smc vddc table init");

	for (i = 0; i < ctx->atb_vddc_tbl.entries_n; ++i) {
		u32 smio_low;

		smio_low = get_unaligned_be32(&smc_state_tbl->smio_low[i]);
		smio_low |= ctx->atb_vddc_tbl.entries[i].smio_low;
		put_unaligned_be32(smio_low, &smc_state_tbl->smio_low[i]);
	}

	put_unaligned_be32(ctx->atb_vddc_tbl.mask_low, 
					&smc_state_tbl->volt_mask_tbl.mask_low[
							SMC_VOLT_MASK_VDDC]);

	/*
	 * The smc needs to know which step has the highest vddc output.
	 * XXX: AMD specs init the max vdcc from atombios pp_info, here
	 * we shorcut with 0.
	 */
	vddc_mv_max = 0;
	max_vddc_idx = 0;
	for (i = 0; i < ctx->atb_vddc_tbl.entries_n; ++i) {
		if (vddc_mv_max < ctx->atb_vddc_tbl.entries[i].val_mv) {
			vddc_mv_max = ctx->atb_vddc_tbl.entries[i].val_mv;
			max_vddc_idx = i;
		}
	}
	smc_state_tbl->max_vddc_idx = max_vddc_idx;
}

static void smc_mvdd_tbl_init(struct ctx *ctx,
					struct smc_state_tbl *smc_state_tbl)
{
	struct dev_drv_data *dd;
	u8 i;

	dd = pci_get_drvdata(ctx->dev);

	LOG("smc mvdd table init");

	for (i = 0; i < ctx->atb_mvddc_tbl.entries_n; ++i) {
		u32 smio_low;

		smio_low = get_unaligned_be32(&smc_state_tbl->smio_low[i]);
		smio_low |= ctx->atb_mvddc_tbl.entries[i].smio_low;
		put_unaligned_be32(smio_low, &smc_state_tbl->smio_low[i]);
	}

	put_unaligned_be32(ctx->atb_mvddc_tbl.mask_low,
					&smc_state_tbl->volt_mask_tbl.mask_low[
							SMC_VOLT_MASK_MVDD]);
}

static void smc_vddci_tbl_init(struct ctx *ctx,
					struct smc_state_tbl *smc_state_tbl)
{
	struct dev_drv_data *dd;
	u8 i;

	dd = pci_get_drvdata(ctx->dev);

	LOG("smc vddci table init");

	for (i = 0; i < ctx->atb_vddci_tbl.entries_n; ++i) {
		u32 smio_low;

		smio_low = get_unaligned_be32(&smc_state_tbl->smio_low[i]);
		smio_low |= ctx->atb_vddci_tbl.entries[i].smio_low;
		put_unaligned_be32(smio_low, &smc_state_tbl->smio_low[i]);
	}

	put_unaligned_be32(ctx->atb_vddci_tbl.mask_low,
					&smc_state_tbl->volt_mask_tbl.mask_low[
							SMC_VOLT_MASK_VDDCI]);
}

static void smc_vddc_phase_shed_tbl_init(struct ctx *ctx,
					struct smc_state_tbl *smc_state_tbl)
{
	struct dev_drv_data *dd;
	u8 i;

	dd = pci_get_drvdata(ctx->dev);

	LOG("smc vddc phase shedding table init");

	for (i = 0; i < ctx->atb_vddc_phase_shed_tbl.entries_n; ++i) {
		u32 smio_low;

		/* phase shed params share smio_low with volt lvls */
		smio_low = get_unaligned_be32(&smc_state_tbl->smio_low[i]);
		smio_low |= ctx->atb_vddc_phase_shed_tbl.entries[i].smio_low;
		put_unaligned_be32(smio_low, &smc_state_tbl->smio_low[i]);
	}

	put_unaligned_be32(ctx->atb_vddc_phase_shed_tbl.mask_low,	
			&smc_state_tbl->phase_mask_tbl.mask_low[
							SMC_VOLT_MASK_VDDC]);
}

/*
 * A volt lvl step is a n-upplet of vddc/mvdd/vddci. Each volt
 * (vddc/mvdd/vddci) have a bit mask stored in volt_mask_tbl. Then all gpio
 * values needed to program a volt lvl are stored in smio_low, and that
 * based on the previous mask.
 * Idem for phase shed step, except the gpio mask is stored in
 * phase_mask_tbl. smio_low masks are shared with the volt lvls, then
 * must work together.
 * The mV values are stored in smc lvl tbls.
 */
static void smc_volt_tbls_init(struct ctx *ctx,
					struct smc_state_tbl *smc_state_tbl)
{
	if (ctx->volt_caps & VOLT_CAPS_VDDC_CTL_ENA)
		smc_vddc_tbl_init(ctx, smc_state_tbl);

	if (ctx->volt_caps & VOLT_CAPS_MVDD_CTL_ENA)
		smc_mvdd_tbl_init(ctx, smc_state_tbl);

	if (ctx->volt_caps & VOLT_CAPS_VDDCI_CTL_ENA)
		smc_vddci_tbl_init(ctx, smc_state_tbl);

	if (ctx->volt_caps & VOLT_CAPS_VDDC_PHASE_SHED_CTL_ENA)
		smc_vddc_phase_shed_tbl_init(ctx, smc_state_tbl);
}

static void smc_dyn_pm_params_init(struct ctx *ctx, struct smc_state_tbl *tbl)
{
	u32 safe_pwr_limit;

	/* units are probably mW */

	put_unaligned_be32(ctx->atb_tdp_limit * 1000, &tbl->dyn_pm_params.tdp_limit);
	put_unaligned_be32(ctx->atb_near_tdp_limit * 1000, &tbl->dyn_pm_params.near_tdp_limit);

	safe_pwr_limit = (ctx->atb_near_tdp_limit * SMC_DYN_PM_TDP_SAFE_LIMIT_PERCENT / 100) * 1000;
	put_unaligned_be32(safe_pwr_limit, &tbl->dyn_pm_params.safe_pwr_limit);
}

long smc_state_tbl_init(struct ctx *ctx, struct smc_state_tbl *tbl)
{
	long r;

	LOG("smc state table init");

	smc_volt_tbls_init(ctx, tbl);

	smc_dyn_pm_params_init(ctx, tbl);

	if (ctx->state & STATE_THERMAL_PROTECTION_ENA)
		tbl->thermal_protection_type = 
					SMC_PP_THERMAL_PROTECTION_TYPE_INTERNAL;
	else
		tbl->thermal_protection_type =
					SMC_PP_THERMAL_PROTECTION_TYPE_NONE;

	if (ctx->platform_caps & ATB_PP_PLATFORM_CAPS_HW_DC)
		tbl->system_flgs |= SMC_PP_SYSTEM_FLGS_GPIO_DC;

	if (ctx->platform_caps & ATB_PP_PLATFORM_CAPS_REGULATOR_HOT)
		/* some buggy bios pitcairn chips don't have that feature */
		if ((ctx->dev->device != 0x6818)
						&& (ctx->dev->device != 0x6819))
			tbl->system_flgs |= SMC_PP_SYSTEM_FLGS_REGULATOR_HOT;

	if (ctx->platform_caps & ATB_PP_PLATFORM_CAPS_STEP_VDDC)
		tbl->system_flgs |= SMC_PP_SYSTEM_FLGS_STEP_VDDC;

	if (ctx->misc_caps & MISC_CAPS_VRAM_IS_GDDR5) {
		tbl->system_flgs |= SMC_PP_SYSTEM_FLGS_GDDR5;
	}
				
	if (ctx->platform_caps & ATB_PP_PLATFORM_CAPS_REVERT_GPIO5_POLARITY)
		tbl->extra_flgs |= SMC_PP_EXTRA_FLGS_GPIO5_POLARITY_HIGH;

	if (ctx->platform_caps & ATB_PP_PLATFORM_CAPS_VRHOT_GPIO_CONFIGURABLE)
		tbl->system_flgs |=
				SMC_PP_SYSTEM_FLGS_REGULATOR_HOT_PROG_GPIO;

	smc_state_tbl_initial_init(ctx, tbl);

	r = smc_state_tbl_emergency_init(ctx, tbl);
	if (r == -SI_ERR)
		return -SI_ERR;

	if (ctx->atb_ulv.lvls_n) {
		r = smc_state_tbl_ulv_init(ctx, tbl);
		if (r == -SI_ERR)
			return -SI_ERR;
	} else {
		tbl->ulv = tbl->initial;
		tbl->ulv_lvl = tbl->initial_lvl;
	}

	/*
	 * when the driver init the gpu, the current driver state is the boot or
	 * initial state since the gpu is expected to be booted and post-ed to
	 * that state
	 */
	tbl->driver = tbl->initial;
	tbl->driver_lvls[0] = tbl->initial_lvl;
	return 0;
}
